package mk.company.fin.service;

import java.util.ArrayList;
import java.util.List;

import mk.company.fin.model.Konto;
import mk.company.fin.model.KontoClass;
import mk.company.fin.model.KontoGroup;
import mk.company.fin.model.KontoSynthetic;
import mk.company.fin.structure.SearchTree;
import mk.company.fin.utils.CoreUtils;
import mk.company.fin.utils.StringEntry;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * Contains various functions for manipulating with kontos.
 * 
 * @author vlado.gjorgjiev
 */
@Service
public class KontoService {

	@Autowired
	private ModelService modelService;
	
	@Autowired
	private MessageService messageService;
	
	
	/* CREATING (FORMING) KONTO */
	
	/**
	 * Saves (new or update) one part of the konto depending on the konto's length.
	 * For example: if the given konto is: 34 (length = 2), then a new konto group
	 * is created. Fist the konto class (in the example: 3) is searched, and
	 * if exist then the group is created, if not an error code is returned.
	 * 
	 * @param konto the konto
	 * @param description the description
	 * @return error message if can't insert
	 */
	public String saveKonto(String konto, String description) {
		String errorMessage = StringUtils.EMPTY;
		if (StringUtils.isNotBlank(konto)) {
			
			// first check the characters
			if (isValidKonto(konto)) {
				if (konto.length() == 1) {
					errorMessage = saveKontoClass(konto, description);
				} else if (konto.length() == 2) {
					errorMessage = saveKontoGroup(konto, description);
				} else if (konto.length() == 3) {
					errorMessage = saveKontoSynthetic(konto, description);
				} else if (konto.length() >= 4) {
					errorMessage = saveKontoAnalythic(konto, description);
				}
			} else {
				errorMessage = messageService.getMessage("konto.invalid.character");
			}
		} else {
			errorMessage = messageService.getMessage("konto.invalid");
		}
		return errorMessage;
	}
	
	
	/**
	 * Checks if the konto contains valid characters.
	 * @param konto the konto to check
	 * @return true if all characters are valid
	 */
	public boolean isValidKonto(String konto) {
		boolean result = true;
		for (int i = 0; i < konto.length(); i++) {
			if (CoreUtils.getKontoCharMappingCode(konto.charAt(0)) < 0) {
				result = false;
				break;
			}
		}
		return result;
	}

	
	public String saveKontoClass(String konto, String description) {
		KontoClass kontoClass = null;
		
		// check if exist
		kontoClass = findKontoClass(konto);
		if (kontoClass == null) {
			
			// create Class
			kontoClass = new KontoClass();
			kontoClass.setValue(konto);
			kontoClass.setDescription(description);
			modelService.save(kontoClass);
			
		} else {
			// update Class
			kontoClass.setDescription(description);
			modelService.update(kontoClass);
		}
		
		return StringUtils.EMPTY;
	}

	public String saveKontoGroup(String konto, String description) {
		// parts
		String klass = Character.toString(konto.charAt(0));
		String value = Character.toString(konto.charAt(1));
		
		// find Klass
		KontoClass kontoClass = findKontoClass(klass);
		if (kontoClass == null) {
			return messageService.getMessage("konto.invalid.classdontexist", new Object[] {klass});
		}
		
		KontoGroup kontoGroup = null;
		
		kontoGroup = findKontoGroup(klass, value);
		if (kontoGroup == null) {
			// create Group
			kontoGroup = new KontoGroup();
			kontoGroup.setValue(value);
			kontoGroup.setDescription(description);
			kontoGroup.setKontoClass(kontoClass);
			modelService.save(kontoGroup);
			
		} else {
			// update
			kontoGroup.setDescription(description);
			modelService.update(kontoGroup);
		}
		return StringUtils.EMPTY;
	}
	
	public String saveKontoSynthetic(String konto, String description) {
		// parts
		String klass = Character.toString(konto.charAt(0));
		String group = Character.toString(konto.charAt(1));
		String value = Character.toString(konto.charAt(2));
		
		// find Klass
		KontoGroup kontoGroup = findKontoGroup(klass, group);
		if (kontoGroup == null) {
			return messageService.getMessage("konto.invalid.groupclassdontexist", new Object[] {klass, group});
		}
		
		KontoSynthetic kontoSynthetic = null;
		
		kontoSynthetic = findKontoSynthetic(klass, group, value);
		if (kontoSynthetic == null) {
			// create Group
			kontoSynthetic = new KontoSynthetic();
			kontoSynthetic.setValue(value);
			kontoSynthetic.setDescription(description);
			kontoSynthetic.setKontoGroup(kontoGroup);
			modelService.save(kontoSynthetic);
			
		} else {
			// update
			kontoSynthetic.setDescription(description);
			modelService.update(kontoSynthetic);
		}
		return StringUtils.EMPTY;
	}
	
	public String saveKontoAnalythic(String konto, String description) {
		// parts
		String klass = Character.toString(konto.charAt(0));
		String group = Character.toString(konto.charAt(1));
		String synthetic = Character.toString(konto.charAt(2));
		String value = konto.substring(3);
		
		// find Klass
		KontoSynthetic kontoSynthetic = findKontoSynthetic(klass, group, synthetic);
		if (kontoSynthetic == null) {
			return messageService.getMessage("konto.invalid.groupclasssyntheticdontexist", new Object[] {klass, group, synthetic});
		}
		
		Konto kontoModel = findKonto(klass, group, synthetic, value);
		
		if (kontoModel == null) {
			// create Group
			kontoModel = new Konto();
			kontoModel.setValue(value);
			kontoModel.setDescription(description);
			kontoModel.setKontoSynthetic(kontoSynthetic);
			modelService.save(kontoModel);
			
		} else {
			// update
			kontoModel.setDescription(description);
			modelService.update(kontoModel);
		}
		return StringUtils.EMPTY;
	}

	
	
	/* FINDING KONTO PARTS FROM DB */
	
	/**
	 * Searches the DB to find the konto class.
	 * @param klass the klass value of the konto
	 * @return the result klass
	 */
	public KontoClass findKontoClass(String klass) {
		// search properties
		List<StringEntry> properties = new ArrayList<StringEntry>(1);
		properties.add(new StringEntry("base.value", "'" + klass + "'"));
		
		// result
		List<KontoClass> result = modelService.findByProperties(KontoClass.class, null, properties);
		return CollectionUtils.isEmpty(result) ? null : result.get(0);
	}
	
	/**
	 * Searches the DB to find the konto group.
	 * @param klass the klass value of the konto
	 * @param group the group value of the konto
	 * @return the result group
	 */
	public KontoGroup findKontoGroup(String klass, String group) {
		// join
		List<StringEntry> joinClasses = new ArrayList<StringEntry>(1);
		joinClasses.add(new StringEntry("base.kontoClass", "c"));
		
		// search properties
		List<StringEntry> properties = new ArrayList<StringEntry>(2);
		properties.add(new StringEntry("base.value", "'" + group + "'"));
		properties.add(new StringEntry("c.value", "'" + klass + "'"));
		
		// result
		List<KontoGroup> result = modelService.findByProperties(KontoGroup.class, joinClasses, properties);
		return CollectionUtils.isEmpty(result) ? null : result.get(0);
	}
	
	/**
	 * Searches the DB to find the konto synthetic.
	 * @param klass the klass value of the konto
	 * @param group the group value of the konto
	 * @param synthetic the synthetic value of the konto
	 * @return the result synthetic
	 */
	public KontoSynthetic findKontoSynthetic(String klass, String group, String synthetic) {
		// join
		List<StringEntry> joinClasses = new ArrayList<StringEntry>(1);
		joinClasses.add(new StringEntry("base.kontoGroup", "g"));
		joinClasses.add(new StringEntry("base.kontoGroup.kontoClass", "c"));
		
		// search properties
		List<StringEntry> properties = new ArrayList<StringEntry>(2);
		properties.add(new StringEntry("base.value", "'" + synthetic + "'"));
		properties.add(new StringEntry("g.value", "'" + group + "'"));
		properties.add(new StringEntry("c.value", "'" + klass + "'"));
		
		// result
		List<KontoSynthetic> result = modelService.findByProperties(KontoSynthetic.class, joinClasses, properties);
		return CollectionUtils.isEmpty(result) ? null : result.get(0);
	}
	
	/**
	 * Searches the DB to find the konto analytic.
	 * @param klass the klass value of the konto
	 * @param group the group value of the konto
	 * @param synthetic the synthetic value of the konto
	 * @return the result synthetic
	 */
	public Konto findKonto(String klass, String group, String synthetic, String analythic) {
		// join
		List<StringEntry> joinClasses = new ArrayList<StringEntry>(1);
		joinClasses.add(new StringEntry("base.kontoSynthetic", "s"));
		joinClasses.add(new StringEntry("base.kontoSynthetic.kontoGroup", "g"));
		joinClasses.add(new StringEntry("base.kontoSynthetic.kontoGroup.kontoClass", "c"));
		
		// search properties
		List<StringEntry> properties = new ArrayList<StringEntry>(2);
		properties.add(new StringEntry("base.value", "'" + analythic + "'"));
		properties.add(new StringEntry("s.value", "'" + synthetic + "'"));
		properties.add(new StringEntry("g.value", "'" + group + "'"));
		properties.add(new StringEntry("c.value", "'" + klass + "'"));
		
		// result
		List<Konto> result = modelService.findByProperties(Konto.class, joinClasses, properties);
		return CollectionUtils.isEmpty(result) ? null : result.get(0);
	}
	
	
	
	/* SEARCHING ONLY ANALYTHIC KONTOS FROM LOCAL CACHE */
	
	private SearchTree searchTree;
	
	/**
	 * Creates a new konto cache.
	 */
	public void createKontoCache() {
		searchTree = new SearchTree();
		List<Konto> allKontos = modelService.findAll(Konto.class);
		for (Konto konto : allKontos) {
			searchTree.addKonto(konto);
		}
	}
	
	/**
	 * Searches the given konto string to find the
	 * corresponding konto (analytic).<br/>
	 * <b>NOTE:</b> Searches the cache. 
	 * @param konto the konto string
	 * @return the result model or null
	 */
	public Konto searchKonto(String konto) {
		return searchTree.searchKonto(konto);
	}
	
	/**
	 * Searches the given konto string to find all
	 * similar kontos (analytic).<br/>
	 * <b>NOTE:</b> Searches the cache.
	 * @param konto the konto string
	 * @return list of models or empty list
	 */
	public List<Konto> searchSimilar(String konto) {
		return searchTree.searchSimilar(konto);
	}
	
	
}
